import os
import ast
import argparse
import webbrowser
import json
from collections import defaultdict


class DependencyScanner:
    def __init__(self, project_root, entry_file, ignore_dirs=None):
        self.project_root = os.path.abspath(project_root)
        self.entry_file = os.path.abspath(entry_file)
        self.ignore_dirs = (
            set(ignore_dirs)
            if ignore_dirs
            else {"__pycache__", ".git", "venv", ".idea"}
        )

        # 存储所有项目内的.py文件
        self.all_py_files = set()
        # 节点字典存储
        self.node_dict = {}
        # 文件之间的依赖关系 (文件 -> 它依赖的文件列表)
        self.dependencies = defaultdict(list)
        # 反向依赖关系 (文件 -> 依赖它的文件列表)
        self.reverse_dependencies = defaultdict(list)
        # 已解析的文件集合
        self.parsed_files = set()

    def collect_py_files(self):
        """收集项目内所有.py文件"""
        for root, dirs, files in os.walk(self.project_root):
            # 过滤忽略目录
            dirs[:] = [d for d in dirs if d not in self.ignore_dirs]

            for file in files:
                if file.endswith(".py"):
                    file_path = os.path.abspath(os.path.join(root, file))
                    self.all_py_files.add(file_path)

    def get_relative_import_path(self, current_path, module_name, level):
        """解析相对导入为绝对路径"""
        # 计算基础目录（向上回溯level-1层）
        base_dir = os.path.dirname(current_path)
        for _ in range(level - 1):
            base_dir = os.path.dirname(base_dir)

        # 构建完整模块路径
        if module_name:
            parts = module_name.split(".")
        else:
            parts = []

        # 尝试可能的文件路径
        target_dir = os.path.join(base_dir, *parts)

        # 检查是否为包目录
        package_init = os.path.join(target_dir, "__init__.py")
        if package_init in self.all_py_files:
            return package_init

        # 检查是否为普通模块
        module_file = target_dir + ".py"
        if module_file in self.all_py_files:
            return module_file

        return None

    def resolve_import(self, current_path, import_name):
        """解析导入语句为文件路径"""
        # 处理相对导入
        if import_name.startswith("."):
            level = 0
            while import_name.startswith("."):
                level += 1
                import_name = import_name[1:]
            return self.get_relative_import_path(current_path, import_name, level)

        # 处理绝对导入
        parts = import_name.split(".")
        # 尝试作为包导入
        package_init = os.path.join(self.project_root, *parts, "__init__.py")
        if package_init in self.all_py_files:
            return package_init

        # 尝试作为模块导入
        module_file = os.path.join(self.project_root, *parts) + ".py"
        if module_file in self.all_py_files:
            return module_file

        return None

    def parse_imports(self, file_path):
        """解析文件中的导入语句"""
        with open(file_path, "r", encoding="utf-8") as file:
            try:
                tree = ast.parse(file.read(), filename=file_path)
            except (SyntaxError, UnicodeDecodeError):
                return set()

        imports = set()

        for node in ast.walk(tree):
            # 处理import语句
            if isinstance(node, ast.Import):
                for alias in node.names:
                    resolved = self.resolve_import(file_path, alias.name)
                    if resolved:
                        imports.add(resolved)

            # 处理from...import语句
            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                level = node.level

                # 构建完整模块名
                full_module = ("." * level) + module
                resolved_base = self.resolve_import(file_path, full_module)

                if resolved_base:
                    imports.add(resolved_base)

        return imports

    def scan_dependencies(self):
        """扫描所有依赖文件"""
        # 收集所有Python文件
        self.collect_py_files()

        # 从入口文件开始扫描
        queue = [self.entry_file]
        self.parsed_files.add(self.entry_file)

        while queue:
            current_file = queue.pop(0)
            imports = self.parse_imports(current_file)

            for imp in imports:
                # 记录依赖关系
                self.dependencies[current_file].append(imp)
                self.reverse_dependencies[imp].append(current_file)

                # 如果新文件未被处理过，加入队列
                if imp not in self.parsed_files:
                    self.parsed_files.add(imp)
                    queue.append(imp)

        # 构建节点字典
        for file in self.all_py_files:
            rel_path = os.path.relpath(file, self.project_root)
            # 确保路径使用正斜杠
            rel_path = rel_path.replace("\\", "/")

            # 检查是否有依赖关系
            import_nodes = []
            if file in self.dependencies:
                for dep in self.dependencies[file]:
                    dep_rel_path = os.path.relpath(dep, self.project_root).replace(
                        "\\", "/"
                    )
                    import_nodes.append(dep_rel_path)

            self.node_dict[rel_path] = {
                "name": os.path.basename(file),
                "path": rel_path,
                "import_nodes": import_nodes,
                "is_entry": file == self.entry_file,
                "is_used": file in self.parsed_files
                or file in self.reverse_dependencies,
            }

    def get_unused_files(self):
        """获取未被依赖的文件列表"""
        # 所有文件减去被依赖的文件
        depended_files = set(self.parsed_files) | set(self.reverse_dependencies.keys())
        return self.all_py_files - depended_files

    def build_directory_tree(self, show_mode="all"):
        """构建项目目录树结构
        show_mode: "all" - 所有文件, "unused" - 仅未使用文件
        """
        dir_tree = {}
        unused_files = self.get_unused_files()

        for file_path in self.all_py_files:
            rel_path = os.path.relpath(file_path, self.project_root).replace("\\", "/")
            parts = rel_path.split("/")
            is_used = file_path not in unused_files

            # 根据显示模式过滤
            if show_mode == "unused" and is_used:
                continue

            current_level = dir_tree

            for part in parts[:-1]:
                if part not in current_level:
                    current_level[part] = {"type": "dir", "children": {}}
                current_level = current_level[part]["children"]

            file_name = parts[-1]
            current_level[file_name] = {
                "type": "file",
                "full_path": rel_path,
                "is_used": is_used,
                "is_entry": file_path == self.entry_file,
            }

        return dir_tree

    def generate_html_visualization(self, output_path):
        """生成HTML可视化文件"""
        # 创建输出目录
        os.makedirs(os.path.dirname(output_path), exist_ok=True)

        # 构建数据结构
        all_files_tree = self.build_directory_tree("all")
        unused_files_tree = self.build_directory_tree("unused")
        unused_count = len(self.get_unused_files())
        entry_path = os.path.relpath(self.entry_file, self.project_root).replace(
            "\\", "/"
        )

        # 生成HTML内容
        html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python Dependency Visualization</title>
    <style>
        * {{
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }}
        
        body {{
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
        }}
        
        .container {{
            max-width: 1400px;
            margin: 0 auto;
            background-color: rgba(0, 0, 0, 0.7);
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
        }}
        
        header {{
            text-align: center;
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 2px solid #4a90e2;
        }}
        
        h1 {{
            font-size: 2.5rem;
            margin: 0 0 10px;
            color: #4a90e2;
            text-shadow: 0 0 10px rgba(74, 144, 226, 0.7);
        }}
        
        .subtitle {{
            font-size: 1.2rem;
            color: #aaa;
            margin-bottom: 20px;
        }}
        
        .dashboard {{
            display: flex;
            gap: 20px;
            margin-bottom: 30px;
        }}
        
        .panel {{
            background: rgba(30, 30, 50, 0.8);
            border-radius: 10px;
            padding: 20px;
            flex: 1;
            height: 800px;
            display: flex;
            flex-direction: column;
        }}
        
        .panel-header {{
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #4a90e2;
        }}
        
        .panel-title {{
            font-size: 1.5rem;
            color: #4a90e2;
        }}
        
        .controls {{
            display: flex;
            gap: 10px;
        }}
        
        button {{
            background: linear-gradient(to right, #4a90e2, #63b3ed);
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 50px;
            cursor: pointer;
            font-size: 0.9rem;
            font-weight: bold;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        }}
        
        button:hover {{
            transform: translateY(-3px);
            box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
            background: linear-gradient(to right, #63b3ed, #4a90e2);
        }}
        
        .stats {{
            display: flex;
            justify-content: space-around;
            background: rgba(30, 30, 50, 0.8);
            border-radius: 10px;
            padding: 15px;
            margin-bottom: 25px;
            flex-wrap: wrap;
        }}
        
        .stat-card {{
            text-align: center;
            padding: 15px;
            min-width: 150px;
        }}
        
        .stat-value {{
            font-size: 2.2rem;
            font-weight: bold;
            color: #4a90e2;
            margin: 5px 0;
        }}
        
        .stat-label {{
            font-size: 1rem;
            color: #aaa;
        }}
        
        .tree-view {{
            padding-left: 20px;
            overflow-y: auto;
            flex: 1;
        }}
        
        .tree-item {{
            padding: 5px 0;
            cursor: pointer;
            transition: all 0.2s;
        }}
        
        .tree-item:hover {{
            background: rgba(255, 255, 255, 0.1);
            border-radius: 5px;
        }}
        
        .dir::before {{
            content: '📁 ';
        }}
        
        .file::before {{
            content: '📄 ';
        }}
        
        .used {{
            color: #2ecc71;
        }}
        
        .unused {{
            color: #e74c3c;
        }}
        
        .entry {{
            color: #4a90e2;
            font-weight: bold;
        }}
        
        .node-container {{
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            padding: 10px;
            overflow-y: auto;
            height: 100%;
        }}
        
        .node-card {{
            background: rgba(74, 144, 226, 0.2);
            border-radius: 8px;
            padding: 15px;
            min-width: 200px;
            cursor: pointer;
            transition: all 0.3s ease;
            border: 1px solid rgba(74, 144, 226, 0.5);
        }}
        
        .node-card:hover {{
            background: rgba(74, 144, 226, 0.4);
            transform: translateY(-5px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }}
        
        .node-header {{
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }}
        
        .node-name {{
            font-weight: bold;
            font-size: 1.1rem;
            color: #4a90e2;
        }}
        
        .entry-node .node-name {{
            color: #4a90e2;
            text-shadow: 0 0 5px rgba(74, 144, 226, 0.7);
        }}
        
        .node-path {{
            font-size: 0.9rem;
            color: #aaa;
            margin-bottom: 10px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }}
        
        .dependencies-list {{
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid rgba(255, 255, 255, 0.1);
        }}
        
        .dependency-item {{
            padding: 5px;
            background: rgba(30, 30, 50, 0.5);
            margin-bottom: 5px;
            border-radius: 4px;
            font-size: 0.9rem;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }}
        
        .legend {{
            display: flex;
            justify-content: center;
            gap: 30px;
            margin-top: 20px;
            flex-wrap: wrap;
        }}
        
        .legend-item {{
            display: flex;
            align-items: center;
            gap: 8px;
        }}
        
        footer {{
            text-align: center;
            margin-top: 30px;
            color: #aaa;
            font-size: 0.9rem;
        }}
        
        .file-count {{
            font-size: 0.9rem;
            color: #aaa;
            margin-top: 5px;
        }}
        
        .message-box {{
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(231, 76, 60, 0.9);
            color: white;
            padding: 15px 30px;
            border-radius: 50px;
            font-weight: bold;
            z-index: 1000;
            opacity: 0;
            transition: opacity 0.3s;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }}
        
        .message-box.show {{
            opacity: 1;
        }}
        
        .no-nodes {{
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100%;
            color: #aaa;
            font-size: 1.2rem;
        }}
    </style>
</head>
<body>
    <div id="message-box" class="message-box">此文件无依赖文件</div>
    
    <div class="container">
        <header>
            <h1>Python 依赖关系可视化</h1>
            <div class="subtitle">项目: {os.path.basename(self.project_root)} | 入口文件: {entry_path}</div>
        </header>
        
        <div class="stats">
            <div class="stat-card">
                <div class="stat-value">{len(self.parsed_files)}</div>
                <div class="stat-label">已使用文件</div>
            </div>
            <div class="stat-card">
                <div class="stat-value">{unused_count}</div>
                <div class="stat-label">未使用文件</div>
            </div>
            <div class="stat-card">
                <div class="stat-value">{len(self.dependencies)}</div>
                <div class="stat-label">依赖关系</div>
            </div>
            <div class="stat-card">
                <div class="stat-value">{len(self.all_py_files)}</div>
                <div class="stat-label">总文件数</div>
            </div>
        </div>
        
        <div class="dashboard">
            <div class="panel">
                <div class="panel-header">
                    <div class="panel-title">文件依赖关系</div>
                    <div class="controls">
                        <button id="reset-dep">返回入口文件</button>
                    </div>
                </div>
                <div id="dependency-view" class="node-container">
                    <!-- 依赖节点将由JavaScript动态生成 -->
                </div>
            </div>
            
            <div class="panel">
                <div class="panel-header">
                    <div class="panel-title">项目文件</div>
                    <div class="controls">
                        <button id="show-all-files" class="active">所有文件</button>
                        <button id="show-unused-files">未使用文件</button>
                        <button id="collapse-dir">折叠全部</button>
                    </div>
                </div>
                <div id="directory-tree" class="tree-view">
                    <!-- 目录树将由JavaScript动态生成 -->
                </div>
                <div id="file-count" class="file-count">
                    总文件数: {len(self.all_py_files)} | 未使用文件: {unused_count}
                </div>
            </div>
        </div>
        
        <div class="legend">
            <div class="legend-item">
                <span class="entry">入口文件</span>
            </div>
            <div class="legend-item">
                <span class="used">已使用文件</span>
            </div>
            <div class="legend-item">
                <span class="unused">未使用文件</span>
            </div>
        </div>
        
        <footer>
            依赖关系扫描工具 | 使用Python AST解析 | {len(self.all_py_files)}个文件, {len(self.dependencies)}个依赖关系
        </footer>
    </div>

    <script>
        // 节点字典数据
        const nodeDict = {json.dumps(self.node_dict, indent=2)};
        
        // 目录树数据
        const allFilesTree = {json.dumps(all_files_tree)};
        const unusedFilesTree = {json.dumps(unused_files_tree)};
        
        // 当前状态
        let currentFileView = "all"; // "all" 或 "unused"
        let currentDependencies = [];
        const entryFilePath = "{entry_path}";
        
        // 查找入口文件节点
        let entryNode = null;
        for (const key in nodeDict) {{
            if (nodeDict[key].path === entryFilePath) {{
                entryNode = nodeDict[key];
                break;
            }}
        }}
        
        // 显示消息
        function showMessage(message) {{
            const msgBox = document.getElementById('message-box');
            msgBox.textContent = message;
            msgBox.classList.add('show');
            
            setTimeout(() => {{
                msgBox.classList.remove('show');
            }}, 2000);
        }}
        
        // 渲染依赖视图
        function renderDependencyView(nodes) {{
            const container = document.getElementById('dependency-view');
            container.innerHTML = '';
            
            if (!nodes || nodes.length === 0) {{
                container.innerHTML = '<div class="no-nodes">当前没有显示任何依赖节点</div>';
                return;
            }}
            
            nodes.forEach(node => {{
                const isEntry = node.path === entryFilePath;
                
                const card = document.createElement('div');
                card.className = `node-card ${{isEntry ? 'entry-node' : ''}}`;
                card.innerHTML = `
                    <div class="node-header">
                        <div class="node-name">${{node.name}}</div>
                    </div>
                    <div class="node-path" title="${{node.path}}">${{node.path}}</div>
                    ${{node.import_nodes.length > 0 ? `
                        <div class="dependencies-title">依赖文件:</div>
                        <div class="dependencies-list">
                            ${{node.import_nodes.map(dep => 
                                `<div class="dependency-item" title="${{dep}}">${{dep.split('/').pop()}}</div>`
                            ).join('')}}
                        </div>
                    ` : ''}}
                `;
                
                // 点击事件
                card.addEventListener('click', function(e) {{
                    if (e.target.classList.contains('dependency-item')) return;
                    
                    // 查找完整节点
                    let fullNode = null;
                    for (const key in nodeDict) {{
                        if (nodeDict[key].path === node.path) {{
                            fullNode = nodeDict[key];
                            break;
                        }}
                    }}
                    
                    if (!fullNode) return;
                    
                    // 获取依赖节点
                    const dependencyPaths = fullNode.import_nodes;
                    
                    if (dependencyPaths.length > 0) {{
                        // 查找依赖节点对象
                        const dependencies = [];
                        for (const path of dependencyPaths) {{
                            if (nodeDict[path]) {{
                                dependencies.push(nodeDict[path]);
                            }}
                        }}
                        
                        // 显示依赖节点
                        currentDependencies = dependencies;
                        renderDependencyView(dependencies);
                    }} else {{
                        showMessage("此文件无依赖文件");
                    }}
                }});
                
                container.appendChild(card);
            }});
        }}
        
        // 渲染目录树
        function renderDirectoryTree(data, container, level = 0) {{
            const keys = Object.keys(data).sort((a, b) => {{
                // 目录在前，文件在后
                if (data[a].type === 'dir' && data[b].type !== 'dir') return -1;
                if (data[a].type !== 'dir' && data[b].type === 'dir') return 1;
                return a.localeCompare(b);
            }});
            
            keys.forEach(key => {{
                const item = data[key];
                const itemElement = document.createElement('div');
                itemElement.className = 'tree-item';
                itemElement.style.paddingLeft = level * 15 + 'px';
                
                if (item.type === 'dir') {{
                    itemElement.classList.add('dir');
                    itemElement.textContent = key;
                    
                    // 添加点击事件展开/折叠
                    itemElement.addEventListener('click', function(e) {{
                        e.stopPropagation();
                        const childrenContainer = this.nextElementSibling;
                        if (childrenContainer.style.display === 'none') {{
                            childrenContainer.style.display = 'block';
                        }} else {{
                            childrenContainer.style.display = 'none';
                        }}
                    }});
                    
                    const childrenContainer = document.createElement('div');
                    childrenContainer.className = 'tree-view';
                    childrenContainer.style.display = 'block'; // 默认展开
                    renderDirectoryTree(item.children, childrenContainer, level + 1);
                    
                    container.appendChild(itemElement);
                    container.appendChild(childrenContainer);
                }} else {{
                    // 文件
                    itemElement.classList.add('file');
                    if (item.is_used) {{
                        itemElement.classList.add('used');
                    }} else {{
                        itemElement.classList.add('unused');
                    }}
                    if (item.is_entry) {{
                        itemElement.classList.add('entry');
                    }}
                    
                    itemElement.textContent = key;
                    itemElement.title = item.full_path;
                    container.appendChild(itemElement);
                }}
            }});
        }}
        
        // 折叠所有目录
        function collapseAllDirectories() {{
            const containers = document.querySelectorAll('#directory-tree .tree-view');
            containers.forEach(container => {{
                    container.style.display = 'none';
            }});
        }}
        
        // 显示所有文件
        function showAllFiles() {{
            document.getElementById('show-all-files').classList.add('active');
            document.getElementById('show-unused-files').classList.remove('active');
            currentFileView = "all";
            
            const dirContainer = document.getElementById('directory-tree');
            dirContainer.innerHTML = '';
            renderDirectoryTree(allFilesTree, dirContainer);
        }}
        
        // 显示未使用文件
        function showUnusedFiles() {{
            document.getElementById('show-all-files').classList.remove('active');
            document.getElementById('show-unused-files').classList.add('active');
            currentFileView = "unused";
            
            const dirContainer = document.getElementById('directory-tree');
            dirContainer.innerHTML = '';
            renderDirectoryTree(unusedFilesTree, dirContainer);
        }}
        
        // 初始化渲染
        function initRender() {{
            // 初始显示入口文件
            if (entryNode) {{
                currentDependencies = [entryNode];
                renderDependencyView([entryNode]);
            }} else {{
                const container = document.getElementById('dependency-view');
                container.innerHTML = '<div class="no-nodes">未找到入口文件节点</div>';
            }}
            
            // 渲染目录树（初始显示所有文件）
            const dirContainer = document.getElementById('directory-tree');
            dirContainer.innerHTML = '';
            renderDirectoryTree(allFilesTree, dirContainer);
        }}
        
        // 重置依赖视图
        document.getElementById('reset-dep').addEventListener('click', function() {{
            if (entryNode) {{
                currentDependencies = [entryNode];
                renderDependencyView([entryNode]);
            }} else {{
                showMessage("未找到入口文件");
            }}
        }});
        
        // 显示所有文件
        document.getElementById('show-all-files').addEventListener('click', showAllFiles);
        
        // 显示未使用文件
        document.getElementById('show-unused-files').addEventListener('click', showUnusedFiles);
        
        // 折叠全部目录
        document.getElementById('collapse-dir').addEventListener('click', collapseAllDirectories);
        
        // 初始渲染
        initRender();
    </script>
</body>
</html>
        """

        # 写入HTML文件
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(html_content)

        return output_path


def main():
    parser = argparse.ArgumentParser(
        description="Python Project Dependency Scanner with Visualization"
    )
    parser.add_argument("project_root", help="Project root directory")
    parser.add_argument(
        "entry_file", help="Project entry file (relative to project root)"
    )
    parser.add_argument(
        "--ignore",
        nargs="*",
        default=["__pycache__", ".git", ".venv", ".idea"],
        help="Directories to ignore",
    )
    parser.add_argument(
        "--output",
        default="dependency_visualization.html",
        help="Output HTML file path",
    )

    args = parser.parse_args()

    # 构建完整入口文件路径
    entry_path = os.path.join(args.project_root, args.entry_file)

    # 检查路径有效性
    if not os.path.isdir(args.project_root):
        print(f"错误：项目目录 '{args.project_root}' 不存在")
        return

    if not os.path.isfile(entry_path):
        print(f"错误：入口文件 '{entry_path}' 不存在")
        return

    # 执行扫描
    print("=" * 60)
    print(f"开始扫描项目: {args.project_root}")
    print(f"入口文件: {args.entry_file}")
    print(f"忽略目录: {', '.join(args.ignore)}")
    print("=" * 60)

    scanner = DependencyScanner(
        project_root=args.project_root, entry_file=entry_path, ignore_dirs=args.ignore
    )
    scanner.scan_dependencies()

    # 生成可视化
    output_path = os.path.abspath(args.output)
    html_path = scanner.generate_html_visualization(output_path)

    # 打开浏览器查看结果
    print("\n扫描完成！")
    print(f"扫描到 {len(scanner.parsed_files)} 个被依赖文件")
    print(f"扫描到 {len(scanner.get_unused_files())} 个未使用文件")
    print(f"生成可视化文件: {html_path}")

    try:
        webbrowser.open(f"file://{html_path}")
        print("已在浏览器中打开可视化结果...")
    except:
        print("无法自动打开浏览器，请手动打开生成的HTML文件")


if __name__ == "__main__":
    main()
